Micron Document
🎖️GitЯра🎖️


Displaying Raw • Download

androidApp/src/google/kotlin/org/meshtastic/app/map/offline/terrain/ContourOverlay.kt renovate/fastlane-2.x-lockfile (056eed64) Text, 9.67 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.app.map.offline.terrain

Tff7b72import T7ee787androidx.compose.runtime.Composable
Tff7b72import T7ee787androidx.compose.runtime.LaunchedEffect
Tff7b72import T7ee787androidx.compose.runtime.NonRestartableComposable
Tff7b72import T7ee787androidx.compose.runtime.getValue
Tff7b72import T7ee787androidx.compose.runtime.key
Tff7b72import T7ee787androidx.compose.runtime.mutableStateOf
Tff7b72import T7ee787androidx.compose.runtime.remember
Tff7b72import T7ee787androidx.compose.runtime.setValue
Tff7b72import T7ee787androidx.compose.ui.graphics.Color
Tff7b72import T7ee787com.google.android.gms.maps.model.LatLng
Tff7b72import T7ee787com.google.maps.android.compose.CameraPositionState
Tff7b72import T7ee787com.google.maps.android.compose.Polyline
Tff7b72import T7ee787kotlinx.coroutines.withContext
Tff7b72import T7ee787org.meshtastic.app.map.offline.pmtiles.OfflineRegion
Tff7b72import T7ee787org.meshtastic.app.map.offline.pmtiles.WebMercatorTileMath
Tff7b72import T7ee787org.meshtastic.core.common.util.ioDispatcher
Tff7b72import T7ee787org.meshtastic.feature.map.terrain.ContourGenerator
Tff7b72import T7ee787org.meshtastic.feature.map.terrain.ContourIntervals
Tff7b72import T7ee787org.meshtastic.feature.map.terrain.ContourLine
Tff7b72import T7ee787org.meshtastic.feature.map.terrain.ContourStyle
Tff7b72import T7ee787org.meshtastic.feature.map.terrain.ContourStyling
Tff7b72import T7ee787org.meshtastic.feature.map.terrain.ElevationTile
Tff7b72import T7ee787org.meshtastic.feature.map.terrain.GeoBounds
Tff7b72import T7ee787org.meshtastic.feature.map.terrain.MapterhornEndpoints
Tff7b72import T7ee787org.meshtastic.feature.map.terrain.TerrainSource
Tff7b72import T7ee787org.meshtastic.feature.map.terrain.TerrainTileMath
Tff7b72import T7ee787org.meshtastic.feature.map.terrain.TerrainTileStore
Tff7b72import T7ee787org.meshtastic.feature.map.terrain.TileIndex
Tff7b72import T7ee787org.meshtastic.feature.map.terrain.decodeTerrariumTile

T8b949e/** One contour line already placed at real [LatLng]s and styled, ready to draw. */
Tff7b72private Tff7b72data Tff7b72class T56d364StyledContourTb4b4b4(Tff7b72val Te6edf3pointsTb4b4b4: Te6edf3ListTff7b72<Te6edf3LatLngTff7b72>Tb4b4b4, Tff7b72val Te6edf3styleTb4b4b4: Te6edf3ContourStyleTb4b4b4, Tff7b72val Te6edf3elevationMetersTb4b4b4: Tffa657FloatTb4b4b4)

T8b949e/**
* Draws elevation contours for whatever's visible on screen, decoding [store]'s downloaded Terrarium tiles and running
* marching-squares ([ContourGenerator]) on demand.
*
* Two things bound the cost of this, since — unlike [HillshadeTileProvider], which only ever runs for the tiles
* Google's own `TileProvider` machinery asks for — this walks its own tile set:
* - **Viewport, not the whole region**: only tiles covering the current visible bounds (plus a one-tile ring so lines
* don't visibly clip right at the edge while panning) are decoded and marched, never the full downloaded region.
* - **Recomputed only once the camera settles** ([CameraPositionState.isMoving] false), not every movement frame —
* marching squares over a couple dozen tiles isn't cheap enough to redo per frame the way the vector overlay's plain
* tile-cache lookup is.
* - **[MAX_CONTOUR_LINES] caps the total line count** actually rendered (Compose has no batched-multi-polyline
* primitive here — confirmed no such thing exists in either MapLibre's or android-maps-utils' changelogs — so every
* line is its own `Polyline` composable). Index lines (thicker, sparser, the ones a glance actually reads) are kept
* preferentially over minor lines via a stable sort, so which lines survive a cap doesn't depend on arbitrary
* tile-iteration order.
*/
Tf0883e@Composable
Tff7b72internal Tff7b72fun Td2a8ffContourOverlayTb4b4b4(
Te6edf3regionTb4b4b4: Te6edf3OfflineRegionTb4b4b4,
Te6edf3storeTb4b4b4: Te6edf3TerrainTileStoreTb4b4b4,
Te6edf3cameraPositionStateTb4b4b4: Te6edf3CameraPositionStateTb4b4b4,
Te6edf3metricTb4b4b4: Tffa657BooleanTb4b4b4,
Tb4b4b4) Tb4b4b4{
Tff7b72var Te6edf3contours Tff7b72by Te6edf3rememberTb4b4b4(Te6edf3regionTb4b4b4.Te6edf3idTb4b4b4) Tb4b4b4{ Te6edf3mutableStateOfTff7b72<Te6edf3ListTff7b72<Te6edf3StyledContourTff7b72>Tff7b72>Tb4b4b4(Te6edf3emptyListTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4}

Te6edf3LaunchedEffectTb4b4b4(Te6edf3regionTb4b4b4.Te6edf3idTb4b4b4, Te6edf3cameraPositionStateTb4b4b4.Te6edf3positionTb4b4b4, Te6edf3cameraPositionStateTb4b4b4.Te6edf3isMovingTb4b4b4, Te6edf3metricTb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3cameraPositionStateTb4b4b4.Te6edf3isMovingTb4b4b4) Tff7b72returnTf0883e@LaunchedEffect
Tff7b72val Te6edf3bounds Tff7b72= Te6edf3cameraPositionStateTb4b4b4.Te6edf3projectionTff7b72?.Te6edf3visibleRegionTff7b72?.Te6edf3latLngBounds Tff7b72?: Tff7b72returnTf0883e@LaunchedEffect
Tff7b72val Te6edf3zoom Tff7b72= Te6edf3cameraPositionStateTb4b4b4.Te6edf3positionTb4b4b4.Te6edf3zoomTb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4)
Te6edf3contours Tff7b72=
Te6edf3withContextTb4b4b4(Te6edf3ioDispatcherTb4b4b4) Tb4b4b4{
Te6edf3computeContoursTb4b4b4(
Te6edf3region Tff7b72= Te6edf3regionTb4b4b4,
Te6edf3store Tff7b72= Te6edf3storeTb4b4b4,
Te6edf3geoBounds Tff7b72=
Te6edf3GeoBoundsTb4b4b4(
Te6edf3south Tff7b72= Te6edf3boundsTb4b4b4.Te6edf3southwestTb4b4b4.Te6edf3latitudeTb4b4b4,
Te6edf3west Tff7b72= Te6edf3boundsTb4b4b4.Te6edf3southwestTb4b4b4.Te6edf3longitudeTb4b4b4,
Te6edf3north Tff7b72= Te6edf3boundsTb4b4b4.Te6edf3northeastTb4b4b4.Te6edf3latitudeTb4b4b4,
Te6edf3east Tff7b72= Te6edf3boundsTb4b4b4.Te6edf3northeastTb4b4b4.Te6edf3longitudeTb4b4b4,
Tb4b4b4)Tb4b4b4,
Te6edf3mapZoom Tff7b72= Te6edf3zoomTb4b4b4,
Te6edf3metric Tff7b72= Te6edf3metricTb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Te6edf3contoursTb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3contour Tff7b72-Tff7b72> Te6edf3keyTb4b4b4(Te6edf3contourTb4b4b4.Te6edf3elevationMetersTb4b4b4, Te6edf3contourTb4b4b4.Te6edf3pointsTb4b4b4) Tb4b4b4{ Te6edf3ContourPolylineTb4b4b4(Te6edf3contourTb4b4b4) Tb4b4b4} Tb4b4b4}
Tb4b4b4}

Tf0883e@Composable
Tf0883e@NonRestartableComposable
Tff7b72private Tff7b72fun Td2a8ffContourPolylineTb4b4b4(Te6edf3contourTb4b4b4: Te6edf3StyledContourTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3baseColor Tff7b72= Te6edf3ColorTb4b4b4(Te6edf3androidTb4b4b4.Te6edf3graphicsTb4b4b4.Te6edf3ColorTb4b4b4.Te6edf3parseColorTb4b4b4(Te6edf3contourTb4b4b4.Te6edf3styleTb4b4b4.Te6edf3strokeHexColorTb4b4b4)Tb4b4b4)
Te6edf3PolylineTb4b4b4(
Te6edf3points Tff7b72= Te6edf3contourTb4b4b4.Te6edf3pointsTb4b4b4,
Te6edf3color Tff7b72= Te6edf3baseColorTb4b4b4.Te6edf3copyTb4b4b4(Te6edf3alpha Tff7b72= Te6edf3contourTb4b4b4.Te6edf3styleTb4b4b4.Te6edf3strokeOpacityTb4b4b4)Tb4b4b4,
Te6edf3width Tff7b72= Te6edf3contourTb4b4b4.Te6edf3styleTb4b4b4.Te6edf3strokeWidthTb4b4b4,
Te6edf3zIndex Tff7b72= Te6edf3CONTOUR_Z_INDEXTb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffcomputeContoursTb4b4b4(
Te6edf3regionTb4b4b4: Te6edf3OfflineRegionTb4b4b4,
Te6edf3storeTb4b4b4: Te6edf3TerrainTileStoreTb4b4b4,
Te6edf3geoBoundsTb4b4b4: Te6edf3GeoBoundsTb4b4b4,
Te6edf3mapZoomTb4b4b4: Tffa657IntTb4b4b4,
Te6edf3metricTb4b4b4: Tffa657BooleanTb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3StyledContourTff7b72> Tb4b4b4{
Tff7b72val Te6edf3terrainZoom Tff7b72= Te6edf3terrainZoomForTb4b4b4(Te6edf3regionTb4b4b4, Te6edf3mapZoomTb4b4b4)
Tff7b72val Te6edf3source Tff7b72= Te6edf3terrainSourceForZoomTb4b4b4(Te6edf3terrainZoomTb4b4b4)

Tff7b72val Te6edf3decodedTiles Tff7b72=
Te6edf3expandedTilesTb4b4b4(Te6edf3terrainZoomTb4b4b4, Te6edf3geoBoundsTb4b4b4)Tb4b4b4.Te6edf3mapNotNull Tb4b4b4{ Te6edf3tile Tff7b72-Tff7b72> Te6edf3decodeTileTb4b4b4(Te6edf3storeTb4b4b4, Te6edf3sourceTb4b4b4, Te6edf3tileTb4b4b4)Tff7b72?.Te6edf3let Tb4b4b4{ Te6edf3tile Te6edf3to Tffa657it Tb4b4b4} Tb4b4b4}
T8b949e// maxOfOrNull, not maxOf: an empty decodedTiles would otherwise throw before the emptiness check below runs.
T8b949e// Falling back to 0f is harmless either way — ContourIntervals.levelsForZoom returns no levels for it either.
Tff7b72val Te6edf3maxElevation Tff7b72= Te6edf3decodedTilesTb4b4b4.Te6edf3maxOfOrNull Tb4b4b4{ Tb4b4b4(Te6edf3_Tb4b4b4, Te6edf3elevationTileTb4b4b4) Tff7b72-Tff7b72> Te6edf3elevationTileTb4b4b4.Te6edf3elevationsTb4b4b4.Te6edf3maxTb4b4b4(Tb4b4b4) Tb4b4b4} Tff7b72?: T79c0ff0f
Tff7b72val Te6edf3levels Tff7b72= Te6edf3ContourIntervalsTb4b4b4.Te6edf3levelsForZoomTb4b4b4(Te6edf3mapZoomTb4b4b4, Te6edf3metricTb4b4b4, Te6edf3maxElevationTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3decodedTilesTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4) Tff7b72|Tff7b72| Te6edf3levelsTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4) Tff7b72return Te6edf3emptyListTb4b4b4(Tb4b4b4)

Tff7b72val Te6edf3allLines Tff7b72=
Te6edf3decodedTilesTb4b4b4.Te6edf3flatMap Tb4b4b4{ Tb4b4b4(Te6edf3tileTb4b4b4, Te6edf3elevationTileTb4b4b4) Tff7b72-Tff7b72>
Te6edf3ContourGeneratorTb4b4b4.Te6edf3generateTb4b4b4(Te6edf3elevationTileTb4b4b4, Te6edf3levelsTb4b4b4)Tb4b4b4.Te6edf3map Tb4b4b4{ Te6edf3line Tff7b72-Tff7b72> Te6edf3tile Te6edf3to Te6edf3line Tb4b4b4}
Tb4b4b4}

T8b949e// Stable sort: lines that are equally "index or not" keep their original (tile-then-generation) order, so
T8b949e// which lines survive MAX_CONTOUR_LINES is deterministic rather than depending on HashMap iteration order.
Tff7b72val Te6edf3ranked Tff7b72=
Te6edf3allLinesTb4b4b4.Te6edf3sortedByDescending Tb4b4b4{ Tb4b4b4(Te6edf3_Tb4b4b4, Te6edf3lineTb4b4b4) Tff7b72-Tff7b72>
Te6edf3ContourIntervalsTb4b4b4.Te6edf3isIndexLevelTb4b4b4(Te6edf3lineTb4b4b4.Te6edf3elevationMetersTb4b4b4, Te6edf3mapZoomTb4b4b4, Te6edf3metricTb4b4b4)
Tb4b4b4}

Tff7b72return Te6edf3rankedTb4b4b4.Te6edf3takeTb4b4b4(Te6edf3MAX_CONTOUR_LINESTb4b4b4)Tb4b4b4.Te6edf3map Tb4b4b4{ Tb4b4b4(Te6edf3tileTb4b4b4, Te6edf3lineTb4b4b4) Tff7b72-Tff7b72> Te6edf3lineTb4b4b4.Te6edf3toStyledContourTb4b4b4(Te6edf3tileTb4b4b4, Te6edf3mapZoomTb4b4b4, Te6edf3metricTb4b4b4) Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72fun Te6edf3ContourLineTb4b4b4.Td2a8fftoStyledContourTb4b4b4(Te6edf3tileTb4b4b4: Te6edf3TileIndexTb4b4b4, Te6edf3zoomTb4b4b4: Tffa657IntTb4b4b4, Te6edf3metricTb4b4b4: Tffa657BooleanTb4b4b4)Tb4b4b4: Te6edf3StyledContour Tb4b4b4{
Tff7b72val Te6edf3points Tff7b72=
Te6edf3pointsTb4b4b4.Te6edf3map Tb4b4b4{ Te6edf3point Tff7b72-Tff7b72>
Te6edf3WebMercatorTileMathTb4b4b4.Te6edf3tileFractionalToLatLngTb4b4b4(
Te6edf3zoom Tff7b72= Te6edf3tileTb4b4b4.Te6edf3zoomTb4b4b4,
Te6edf3tileX Tff7b72= Te6edf3tileTb4b4b4.Te6edf3xTb4b4b4,
Te6edf3tileY Tff7b72= Te6edf3tileTb4b4b4.Te6edf3yTb4b4b4,
Te6edf3fracX Tff7b72= Te6edf3pointTb4b4b4.Te6edf3xTb4b4b4.Te6edf3toDoubleTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3fracY Tff7b72= Te6edf3pointTb4b4b4.Te6edf3yTb4b4b4.Te6edf3toDoubleTb4b4b4(Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tff7b72return Te6edf3StyledContourTb4b4b4(Te6edf3pointsTb4b4b4, Te6edf3ContourStylingTb4b4b4.Te6edf3styleForTb4b4b4(Tff7b72thisTb4b4b4, Te6edf3zoomTb4b4b4, Te6edf3metricTb4b4b4)Tb4b4b4, Te6edf3elevationMetersTb4b4b4)
Tb4b4b4}

T8b949e/** Clamps the map's own zoom to whatever depth this region's terrain download actually reached. */
Tff7b72private Tff7b72fun Td2a8ffterrainZoomForTb4b4b4(Te6edf3regionTb4b4b4: Te6edf3OfflineRegionTb4b4b4, Te6edf3mapZoomTb4b4b4: Tffa657IntTb4b4b4)Tb4b4b4: Tffa657Int Tb4b4b4{
Tff7b72val Te6edf3maxAvailable Tff7b72=
Tff7b72if Tb4b4b4(Te6edf3regionTb4b4b4.Te6edf3terrainHasRegionalDetailTb4b4b4) Tb4b4b4{
Te6edf3MapterhornEndpointsTb4b4b4.Te6edf3REGIONAL_MAX_ZOOM
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3MapterhornEndpointsTb4b4b4.Te6edf3GLOBAL_MAX_ZOOM
Tb4b4b4}
Tff7b72return Te6edf3mapZoomTb4b4b4.Te6edf3coerceInTb4b4b4(T79c0ff0Tb4b4b4, Te6edf3maxAvailableTb4b4b4)
Tb4b4b4}

T8b949e/** [TerrainTileMath.tilesAt] for [bounds], plus a one-tile ring so contours don't visibly clip at the viewport edge. */
Tff7b72private Tff7b72fun Td2a8ffexpandedTilesTb4b4b4(Te6edf3zoomTb4b4b4: Tffa657IntTb4b4b4, Te6edf3boundsTb4b4b4: Te6edf3GeoBoundsTb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3TileIndexTff7b72> Tb4b4b4{
Tff7b72val Te6edf3base Tff7b72= Te6edf3TerrainTileMathTb4b4b4.Te6edf3tilesAtTb4b4b4(Te6edf3zoomTb4b4b4, Te6edf3boundsTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3baseTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4) Tff7b72return Te6edf3base

Tff7b72val Te6edf3maxTileIndex Tff7b72= Tb4b4b4(T79c0ff1 Te6edf3shl Te6edf3zoomTb4b4b4) Tff7b72- T79c0ff1
Tff7b72val Te6edf3minX Tff7b72= Tb4b4b4(Te6edf3baseTb4b4b4.Te6edf3minOf Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3x Tb4b4b4} Tff7b72- Te6edf3TILE_MARGINTb4b4b4)Tb4b4b4.Te6edf3coerceAtLeastTb4b4b4(T79c0ff0Tb4b4b4)
Tff7b72val Te6edf3maxX Tff7b72= Tb4b4b4(Te6edf3baseTb4b4b4.Te6edf3maxOf Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3x Tb4b4b4} Tff7b72+ Te6edf3TILE_MARGINTb4b4b4)Tb4b4b4.Te6edf3coerceAtMostTb4b4b4(Te6edf3maxTileIndexTb4b4b4)
Tff7b72val Te6edf3minY Tff7b72= Tb4b4b4(Te6edf3baseTb4b4b4.Te6edf3minOf Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3y Tb4b4b4} Tff7b72- Te6edf3TILE_MARGINTb4b4b4)Tb4b4b4.Te6edf3coerceAtLeastTb4b4b4(T79c0ff0Tb4b4b4)
Tff7b72val Te6edf3maxY Tff7b72= Tb4b4b4(Te6edf3baseTb4b4b4.Te6edf3maxOf Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3y Tb4b4b4} Tff7b72+ Te6edf3TILE_MARGINTb4b4b4)Tb4b4b4.Te6edf3coerceAtMostTb4b4b4(Te6edf3maxTileIndexTb4b4b4)

Tff7b72val Te6edf3tiles Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3TileIndexTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72for Tb4b4b4(Te6edf3x Tff7b72in Te6edf3minXTb4b4b4.Tb4b4b4.Te6edf3maxXTb4b4b4) Tb4b4b4{
Tff7b72for Tb4b4b4(Te6edf3y Tff7b72in Te6edf3minYTb4b4b4.Tb4b4b4.Te6edf3maxYTb4b4b4) Tb4b4b4{
Te6edf3tiles Tff7b72+Tff7b72= Te6edf3TileIndexTb4b4b4(Te6edf3zoomTb4b4b4, Te6edf3xTb4b4b4, Te6edf3yTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tff7b72return Te6edf3tiles
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffdecodeTileTb4b4b4(Te6edf3storeTb4b4b4: Te6edf3TerrainTileStoreTb4b4b4, Te6edf3sourceTb4b4b4: Te6edf3TerrainSourceTb4b4b4, Te6edf3tileTb4b4b4: Te6edf3TileIndexTb4b4b4)Tb4b4b4: Te6edf3ElevationTile? Tff7b72=
Te6edf3storeTb4b4b4.Te6edf3readTileTb4b4b4(Te6edf3sourceTb4b4b4, Te6edf3tileTb4b4b4)Tff7b72?.Te6edf3let Tb4b4b4{ Te6edf3bytes Tff7b72-Tff7b72>
Tff7b72try Tb4b4b4{
Te6edf3decodeTerrariumTileTb4b4b4(Te6edf3bytesTb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3_Tb4b4b4: Te6edf3IllegalStateExceptionTb4b4b4) Tb4b4b4{
Tff7b72null
Tb4b4b4}
Tb4b4b4}

T8b949e/** Above the offline vector layer's own water/roads/boundaries, still below markers and waypoints. */
Tff7b72private Tff7b72const Tff7b72val Te6edf3CONTOUR_Z_INDEX Tff7b72= Tff7b72-T79c0ff0.6f
Tff7b72private Tff7b72const Tff7b72val Te6edf3TILE_MARGIN Tff7b72= T79c0ff1

T8b949e/**
* A generous, not scientifically tuned, ceiling on rendered contour lines — chosen so a steep-terrain region at a deep
* zoom (many minor levels, many tiles) can't emit thousands of `Polyline` composables. Revisit once this renders on a
* real device; no display was available to judge it against in this environment.
*/
Tff7b72private Tff7b72const Tff7b72val Te6edf3MAX_CONTOUR_LINES Tff7b72= T79c0ff3T79c0ff0T79c0ff0

Served by rngit 1.5.3 - Generated in 0.13s